Classes are quite different in python than in other languages. One way in which they are different is that you can add attributes, or member variables and function, any time, even long after the class is defined.
Say we want to define a class to hold a node in a search tree.
The simplest class definition is
class Node:
pass
Python uses pass/
or an empty statement or body.
This class has no attributes, right? Watch this.
n = Node()
n
n.a = 42
n
n.a
n.a = 43
n.a
Simply assigning a value to something that looks like you are accessing a member variable creates it, but only for that instance.
one = Node()
two = Node()
one.x = 42
one.x
two.x
We can assign a new class attribute, though.
Node.cx = 'node class'
one.cx
two.cx
So, python is very flexible, too flexible some say. We really should
define the attributes in class methods, like the constructor. The
constructor for a class has the special name __init__
.
Here is how we should define our Node
class. Let's say it should
hold a state
, and values for h
, g
, and f
.
class Node:
def __init__(self, state, f,g,h):
self.state = state
self.f = f
self.g = g
self.h = h
Now we can use it like this.
a = Node([1, 2, 3], 0, 0, 0)
a.f
a
The form that is printed when you evaluate it is kind of ugly. In
python, there are two kinds of toString
type of methods, for two
purposes:
__repr__
is meant to display a valid python expression that could be used to generate the value__str__
is meant to display a more human-oriented string that is not meant to be valid python code.Sometimes the __repr__
result is good enough for humans, too.
Here is an example for our Node
class.
class Node:
def __init__(self, state, f, g, h):
self.state = state
self.f = f
self.g = g
self.h = h
def __repr__(self):
return 'Node({}, {}, {}, {})'.format(self.state, self.f, self.g, self.h)
a = Node([1, 2, 3], 0, 0, 0)
a
We can define default values in the constructor, too. This allows f
, g
, and h
to be entered as keyword arguments. And, therefore, the __repr__
form becomes even more readable.
class Node:
def __init__(self, state, f=0, g=0, h=0):
self.state = state
self.f = f
self.g = g
self.h = h
def __repr__(self):
return 'Node({}, f={}, g={}, h={})'.format(self.state, self.f, self.g, self.h)
a = Node([1, 2, 3], 0, 0, 0)
a
b = Node([3,2,3])
b
Sorting a list is easy. sorted
produces a new list that is sorted. The sort
method destructively sorts the list.
nums = [5, 2, 44, 8, 322, 54, 22]
numsSorted = sorted(nums)
numsSorted
nums
nums.sort()
nums
But, what if the things we are sorting are structured and you want to sort by just one or some of the values? Say you have a list of tuples and want to sort by the second value? The sorted
and sort
functions take a key
argument whose value is a function.
pairs = [('a',54), ('b',52), ('c', 2), ('d', 21), ('e', 31)]
pairs
pairs
sorted(pairs, key = lambda p: p[1])
pairs
pairs.sort(key=lambda p: p[1])
pairs
Hey, how about sorting nodes??? Here is list of unexpanded nodes, maybe from someplace in the middle of an A* search.
unExpanded = [Node([3,2,1],2,1,1),
Node([2,1,3],4,2,2),
Node([3,1,2],3,1,2),
Node([1,3,2],1,1,0)]
unExpanded
What do we want to order them by? How would you do this in python?
unExpanded.sort()
unExpanded
Hummm.....nope. How about
unExpanded.sort(key=lambda n: n.f)
unExpanded
That's better. Now we can get the lowest-f node by unExpanded[0]
or get and remove it by unExpanded.pop(0)
. We can also get the second-lowest f node by unExpanded[1]
.
best = unExpanded[0]
best.f
best.state
The multiple lines of an if-else block can be written more compactly, and some might say more intuitively. See this PEP on conditional expressions. (Hey, what does PEP stand for?)
What happens when you try to index beyond the end of a list?
stuff = ['a', 'c', 'x']
stuff[0]
stuff[2]
stuff[3]
So we should surround cases like this with try-except
blocks. But, what if we just want an empty list if our index is beyond the end?
i = 4
if i < len(stuff):
result = stuff[i]
else:
result = []
result
That's a bit clunky. Conditional expressions to the rescue.
result = stuff[i] if i < len(stuff) else []
result
The first expression is not evaluated if the if
condition is false.
We are going to play with some robot movement problems where the robot can move in discrete steps across the floor. To represent a bird's-eye view of the world, let's use an array.
The numpy
module in python is an efficient implementation of arrays. Let's create a 4x4 array of characters to represent a world in which the robot can be in 16 different positions. The position of the robot is marked with 'r' and every other element is a blank.
import numpy as np
world = np.array([
[' ', ' ', ' ', ' '],
[' ', 'r', ' ', ' '],
[' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' ']])
world
How can we move the robot down one step? Index into the array with two indices. But first, here is a cool python idiom for swapping values.
x = 42
y = 100
(x, y)
x, y = y, x
x, y
So, the 'down' step can be done by
world
world[2,1], world[1,1] = world[1,1], world[2,1]
world